package bsh.impl;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import bsh.ReflectionUtils;
import bsh.TypeResolver;
import bsh.compilation.ReflectionUtilsImpl;
import bsh.meta.JavaClassWrapper;
import bsh.meta.TypeWrapper;

/**
 * TypeResolver
 * 
 * @author takeshi
 */
public class CachedTypeResolver implements TypeResolver {

    private final ReflectionUtils reflectionUtils;

    private final Map<String, TypeWrapper> imports = new HashMap<String, TypeWrapper>();

    private final Set<String> wildCards = new HashSet<String>();

    private final Map<String, Object> cache = new HashMap<String, Object>();

    /**
     * C'tor
     * 
     * @param reflectionUtils {@link ReflectionUtils}
     */
    public CachedTypeResolver(ReflectionUtils reflectionUtils) {
        this.reflectionUtils = reflectionUtils;
        this.wildCards.add("java.lang.");
        this.wildCards.add("java.io.");
        addCommonTypeAliases();
    }

    /**
     * Constructor that copies imports from a parent
     * 
     * @param parent {@link TypeResolver}
     */
    public CachedTypeResolver(TypeResolver parent) {
        this.reflectionUtils = parent.getReflectionUtils();
        copyTypeAliases(parent);
    }

    /**
     * @param parent the {@link TypeResolver}
     */
    public void copyTypeAliases(TypeResolver parent) {
        for (String wild : parent.getWildCards()) {
            addWildcardImport(wild);
        }
        for (Entry<String, TypeWrapper> entry : parent.getImports().entrySet()) {
            this.addTypeAlias(entry.getKey(), entry.getValue());
        }
    }

    /**
     * C'tor
     */
    public CachedTypeResolver() {
        this.reflectionUtils = new ReflectionUtilsImpl();
        this.wildCards.add("java.lang.");
        this.wildCards.add("java.io.");
        addCommonTypeAliases();
    }

    /**
     * Adiciona algumas das classes do pacote java.lang._,
     */
    private void addCommonTypeAliases() {
        addTypeAlias(Boolean.class);
        addTypeAlias(Byte.class);
        addTypeAlias(Character.class);
        addTypeAlias(Class.class);
        addTypeAlias(ClassLoader.class);
        addTypeAlias(Double.class);
        addTypeAlias(Enum.class);
        addTypeAlias(Float.class);
        addTypeAlias(Integer.class);
        addTypeAlias(Long.class);
        addTypeAlias(Math.class);
        addTypeAlias(Number.class);
        addTypeAlias(Object.class);
        addTypeAlias(Process.class);
        addTypeAlias(ProcessBuilder.class);
        addTypeAlias(Runtime.class);
        addTypeAlias(Short.class);
        addTypeAlias(String.class);
        addTypeAlias(StringBuffer.class);
        addTypeAlias(StringBuilder.class);
        addTypeAlias(System.class);
        addTypeAlias(Thread.class);
        addTypeAlias(Throwable.class);
        addTypeAlias(Exception.class);
        addTypeAlias(Void.class);
    }

    /**
     * @param any Class
     */
    public void addTypeAlias(Class<?> any) {
        String name = any.getName().substring(any.getName().lastIndexOf('.') + 1);
        addTypeAlias(name, JavaClassWrapper.wrapperFor(any));
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.TypeResolver#addTypeAlias(java.lang.String, bsh.meta.TypeWrapper)
     */
    public synchronized void addTypeAlias(String name, TypeWrapper type) {
        this.imports.put(name, type);
        cache.remove(name);
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.TypeResolver#addWildcardImport(java.lang.String)
     */
    public synchronized void addWildcardImport(String type) {
        if (type.endsWith("*") || type.endsWith(";")) {
            this.addWildcardImport(type.substring(0, type.length() - 1));
        } else if (!type.endsWith(".")) {
            this.addWildcardImport(type + ".");
        } else {
            this.wildCards.add(type);
            this.cache.clear();
        }
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.TypeResolver#resolveType(java.lang.String)
     */
    public synchronized TypeWrapper resolveType(String name) {
        Object cached = cache.get(name);
        if (cached != null) {
            if (cached == NotFound.NOT_FOUND) {
                return null;
            }
            return (TypeWrapper) cached;
        }
        TypeWrapper theType = doResolveType(name);
        if (theType == null) {
            cache.put(name, NotFound.NOT_FOUND);
        } else {
            cache.put(name, theType);
        }
        return theType;
    }

    /**
     * Resolve o tipo usando os wildcard imports se necessario
     * 
     * @param name String
     * @return {@link TypeWrapper}
     */
    private TypeWrapper doResolveType(String name) {
        TypeWrapper resolved = this.imports.get(name);
        if (resolved != null) {
            return resolved;
        }
        resolved = this.reflectionUtils.resolveType(name);
        if (resolved != null) {
            return resolved;
        }
        for (String pack : this.wildCards) {
            resolved = this.reflectionUtils.resolveType(pack + name);
            if (resolved != null) {
                return resolved;
            }
        }
        if (name.contains(".")) {
            String[] split = name.split("[.]");
            TypeWrapper wrapper = resolveType(split[0]);
            if (wrapper != null) {
                return resolveInnerType(wrapper, split, 1);
            }
        }

        return null;
    }

    /**
     * Resolves an inner type or inner type chain
     * 
     * @param wrapper {@link TypeWrapper}
     * @param split the array of types
     * @param i the index on the array
     * @return {@link TypeWrapper} or <code>null</code>
     */
    private TypeWrapper resolveInnerType(TypeWrapper wrapper, String[] split, int i) {
        if (split.length == i) {
            return wrapper; // found it
        }
        Collection<? extends TypeWrapper> innerTypes = wrapper.getInnerTypes();
        for (TypeWrapper typeWrapper : innerTypes) {
            if (split[i].equals(typeWrapper.simpleName())) {
                return resolveInnerType(typeWrapper, split, i + 1);
            }
        }
        return null;
    }

    /**
     * Represents an entry that is not found
     * 
     * @author takeshi
     */
    private static enum NotFound {
        NOT_FOUND
    }

    /**
     * @return the reflectionUtils
     */
    public ReflectionUtils getReflectionUtils() {
        return reflectionUtils;
    }

    /**
     * @return the imports
     */
    public synchronized Map<String, TypeWrapper> getImports() {
        return new HashMap<String, TypeWrapper>(this.imports);
    }

    /**
     * @return the wildCards
     */
    public synchronized Set<String> getWildCards() {
        return new HashSet<String>(this.wildCards);
    }

}
